package middleware;

import java.util.ArrayList;
import java.util.Collections;
import java.io.*;

public class Base64url {
    public static int toDec(int[] n, int base) {
        int sum = 0;
        for (int i = 0; i < n.length; i++) {
            sum += n[n.length - 1 -i] * Math.pow(base, i);
        }
        return sum;
    }

    public static int[] fromDec(int n, int base) {
        ArrayList<Integer> digits = new ArrayList<Integer>();
        int num = n;
        int rest;

        do {
            rest = num % base;
            num = num / base;

            digits.add(rest);
        } while (num != 0);

        Collections.reverse(digits);
        
        /* Convert from ArrayList to int[] */
        int[] result = new int[digits.size()];
        for (int i = 0; i < result.length; i++) {
            result[i] = digits.get(i);
        }
        return result;
    }

    public static int asciiToBase64(int ascii) {
        int val;
        if (ascii >= 65 && ascii <= 90) {
            val = ascii - 65;
        } else if (ascii >= 97 && ascii <= 122) {
            val = ascii - 71;
        } else if (ascii >= 48 && ascii <= 57) {
            val = ascii + 4;
        } else if (ascii == '-') { 
            val = 62;
        } else if (ascii == '_') {
            val = 63;
        } else {
            val = -1;
        }
        return val;
    }

    public static int base64ToAscii(int b) {
        int val;
        if (b >= 0 && b <= 25) {
            val = 65 + b;
        } else if (b >= 26 && b <= 51) { 
            val = 71 + b;
        } else if (b >= 52 && b <= 61) { 
            val = -4 + b;
        } else if (b == 62) {
            val = '-';
        } else if (b == 63) {
            val = '_';
        } else {
            val = -1;
        }
        return val;
    }

    public static String encode(int[] data) {
        int out = 4;
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < data.length; i += 3) {
            int[] values = new int[] {0, 0, 0};
            if (i + 2  < data.length) {
                values[0] = data[i+0];
                values[1] = data[i+1];
                values[2] = data[i+2];
                out = 4;
            } else if (i + 1 < data.length) {
                values[0] = data[i+0];
                values[1] = data[i+1];
                out = 3;
            } else {
                values[0] = data[i+0];
                out = 2;
            }

            int num = toDec(values, 256);
            int[] digits = fromDec(num, 64);

            if (out == 2) { 
                if (digits.length == 1) {
                    sb.append((char) base64ToAscii(0));
                }
                for (int k = 0; k < digits.length && k < 2; k++) {
                    sb.append((char) base64ToAscii(digits[k]));
                }
            } else if (out == 3) {
                if (digits.length == 1) {
                    sb.append((char) base64ToAscii(0));
                    sb.append((char) base64ToAscii(0));
                } else if (digits.length == 2) {
                    sb.append((char) base64ToAscii(0));
                }
                for (int k = 0; k < digits.length && k < 3; k++) {
                    sb.append((char) base64ToAscii(digits[k]));
                }
            } else {
                if (digits.length == 1) {
                    sb.append((char) base64ToAscii(0));
                    sb.append((char) base64ToAscii(0));
                    sb.append((char) base64ToAscii(0));
                } else if (digits.length == 2) {
                    sb.append((char) base64ToAscii(0));
                    sb.append((char) base64ToAscii(0));
                } else if (digits.length == 3) {
                    sb.append((char) base64ToAscii(0));
                }
                for (int k = 0; k < digits.length && k < 4; k++) {
                    sb.append((char) base64ToAscii(digits[k]));
                }
            }
        }

        return sb.toString();
    }

    public static int[] decode(String data) {
        ArrayList<Integer> tempBytes = new ArrayList<Integer>();
        int[] values = null;
        int[] chars = null;
        int shift = 0;

        for (int i = 0; i < data.length(); i += 4) {
            values = new int[] {0, 0, 0, 0};
            chars = new int[] {0, 0, 0, 0};
            if (i + 3 < data.length()) {
                chars[0] = data.charAt(i+0);
                chars[1] = data.charAt(i+1);
                chars[2] = data.charAt(i+2);
                chars[3] = data.charAt(i+3);
            } else if (i + 2 < data.length()) {
                chars[1] = data.charAt(i+0);
                chars[2] = data.charAt(i+1);
                chars[3] = data.charAt(i+2);
                shift = 2;
            } else if (i + 1 < data.length()) {
                chars[2] = data.charAt(i+0);
                chars[3] = data.charAt(i+1);
                shift = 4;
            } else {
                System.out.println("ERROR");
                return null;
            }
            
            for (int l = 0; l < chars.length; l++) {
                if (chars[l] != 0) {
                    values[l] = asciiToBase64(chars[l]);
                } else { 
                    values[l] = 0;
                }
            }

            int num = toDec(values, 64);
            num = num >> shift;
            int[] digits = fromDec(num, 256);

            if (digits.length == 1) {
                if (i + 3 < data.length()) {
                   tempBytes.add(0); 
                   tempBytes.add(0); 
                } else if (i + 2 < data.length()) {
                   tempBytes.add(0); 
                }
            } else if (digits.length == 2) {
                if (i + 3 < data.length()) {
                   tempBytes.add(0); 
                }
            }

            for (int k : digits) {
                tempBytes.add(k);
            }
        }

        int[] result = new int[tempBytes.size()];
        for (int j = 0; j < tempBytes.size(); j++) {
            result[j] = tempBytes.get(j);
        }

        return result;
    }

    public static void decodeFile(String in, String out) throws IOException {
        StringBuilder sb = new StringBuilder();
        FileReader fr = new FileReader(in);
        int c;
        int i = 0;
        while ( (c = fr.read()) != -1) {
            sb.append((char) c);
            i++;
        }

        FileOutputStream fos = new FileOutputStream(out);
        for (int b : decode(sb.toString().trim())) {
            fos.write((byte) b);
        } 
        fos.close();
    }    

    public static void encodeFile(String in, String out) throws IOException {
        ArrayList<Integer> bytesTemp = new ArrayList<Integer>();
        FileInputStream fis = new FileInputStream(in);
        int c;
        while ( (c = fis.read()) != -1) {
            bytesTemp.add(c);
        }
        fis.close();

        int[] bytes = new int[bytesTemp.size()];
        for (int i = 0; i < bytesTemp.size(); i++) {
            bytes[i] = bytesTemp.get(i);
        }

        String s = encode(bytes);
        
        FileWriter fw = new FileWriter(out);
        fw.write(s);
        fw.close();
    }

    public static void usage() {
        System.err.println("Usage: java Base64url [decode|encode] <in-file> <out-file>");
        System.exit(1);
    }

    public static void main(String[] args) throws IOException {
        if (args.length != 3) {
            usage();
        }

        String inFile = args[1];
        String outFile = args[2];

        if (args[0].equals("decode")) {
            decodeFile(inFile, outFile);
        } else if (args[0].equals("encode")) {
            encodeFile(inFile, outFile);
        } else {
            usage();
        }
    }    
}
